package org.basket.core;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.mail.Message;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

import org.basket.xml.XML;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.QueryResultIterator;

/*
 * current schema has these objects:
 * AdminUser [kind:"AdminUser", id:"AdminUser"][password: "password"]
 * BasketUser [kind:"BasketUser", id:"email"][name: "user name"]
 * BasketMessage [parent: messageParent, kind:"date(YYYY-MM-DD)", id:"email"][presence: Boolean, ball: Boolean, message: "the message"]
 * messageParent = key[kind:"messageParent", id:"messageParent"]
 */
public class DBManager {

	private static final Logger log = Logger.getLogger(DBManager.class.getName());

	static final DatastoreService ds = DatastoreServiceFactory.getDatastoreService();

	/** cache of all days messages */
	private static Map<String, DayRecord> dayRecordHashStore = null;

	/** cache of all users */
	private static List<String> allUserMails = null;

	/*
	 * <db> <entity +kind +name/id ?parentKind ?parentName/parentId> <property
	 * name type>value</property>
	 */
	public static String DBExport() {
		StringBuilder buffer = new StringBuilder();
		buffer.append("<db>\r\n");

		{
			PreparedQuery pq = ds.prepare(new Query(AdminUser.KIND));
			QueryResultIterator<Entity> iterator = pq.asQueryResultIterator();
			while (iterator.hasNext()) {
				final Entity entity = iterator.next();
				appendEntity(buffer, entity);
			}
		}

		{
			PreparedQuery pq = ds.prepare(new Query(BasketUser.KIND));
			QueryResultIterator<Entity> iterator = pq.asQueryResultIterator();
			while (iterator.hasNext()) {
				final Entity entity = iterator.next();
				appendEntity(buffer, entity);
			}
		}
		{
			PreparedQuery pq = ds.prepare(new Query(BasketMessage.KIND));
			QueryResultIterator<Entity> iterator = pq.asQueryResultIterator();
			while (iterator.hasNext()) {
				final Entity entity = iterator.next();
				appendEntity(buffer, entity);
			}
		}

		buffer.append("</db>");
		return buffer.toString();
	}

	private static void appendEntity(StringBuilder buffer, Entity entity) {
		final Key key = entity.getKey();
		appendKey(buffer, key);
		final Map<String, Object> properties = entity.getProperties();
		for (Entry<String, Object> property : properties.entrySet()) {
			final Object value = property.getValue();
			if (value != null) {
				buffer.append("\t\t<property name=\"");
				buffer.append(property.getKey());
				buffer.append("\" type=\"").append(value.getClass().getName()).append("\">");
				buffer.append(XML.escape(value.toString()));
				buffer.append("</property>\r\n");
			}
		}
		buffer.append("\t</entity>\r\n");
	}

	// <entity +kind +name/id ?parentKind ?parentName/parentId>
	private static void appendKey(StringBuilder buffer, Key key) {
		buffer.append("\t<entity ");
		{
			String kind = key.getKind();
			if (kind != null) {
				buffer.append("kind=\"").append(kind).append("\" ");
			}
		}
		{
			String name = key.getName();
			if (name != null) {
				buffer.append("name=\"").append(name).append("\" ");
			} else {
				buffer.append("id=\"").append(key.getId()).append("\" ");
			}
		}
		{
			Key parent = key.getParent();
			if (parent != null) {
				{
					String kind = parent.getKind();
					if (kind != null) {
						buffer.append("parentKind=\"").append(kind).append("\" ");
					}
				}
				{
					String name = parent.getName();
					if (name != null) {
						buffer.append("parentName=\"").append(name).append("\" ");
					} else {
						buffer.append("parentId=\"").append(parent.getId()).append("\" ");
					}
				}
			}
		}
		buffer.append(">\r\n");
	}

	/**
	 * @return currently defined administration password
	 */
	public static String getAdminPass() {
		try {
			Entity entity = ds.get(AdminUser.key);
			return (String) entity.getProperty(AdminUser.PASSWORD);
		} catch (EntityNotFoundException e) {
			return null;
		}
	}

	/**
	 * used in getMonthMessages as result
	 */
	public static final class DayRecord {
		public int presence = 0;
		public int total = 0;
		public boolean ball = false;
	}

	/**
	 * put entity in the DB
	 */
	public static void store(EntityAtom entity) {
		Entity e = entity.getEntity();
		ds.put(e);
		if (entity instanceof BasketMessage) {
			dayRecordHashStore = null;
		}
	}

	/**
	 * delete entity from DB
	 */
	public static void delete(EntityAtom entity) {
		Entity e = entity.getEntity();
		ds.delete(e.getKey());
		if (entity instanceof BasketMessage) {
			dayRecordHashStore = null;
		}
	}

	/**
	 * Returns all messages statistic as Map<String, DayRecord>
	 * 
	 * @return all messages statistic as Map<String, DayRecord>
	 */
	public static synchronized Map<String, DayRecord> getMonthMessages() {
		if (dayRecordHashStore == null) {
			clearOldMeassages();

			final Map<String, DayRecord> messages = new HashMap<String, DayRecord>();

			Query query = new Query(BasketMessage.KIND);
			PreparedQuery pq = ds.prepare(query);
			QueryResultIterator<Entity> result = pq.asQueryResultIterator();

			while (result.hasNext()) {
				Entity entity = result.next();
				String recordDate = (String) entity.getProperty(BasketMessage.DATE);

				DayRecord day = messages.get(recordDate);
				if (day == null) {
					day = new DayRecord();
					messages.put(recordDate, day);
				}
				day.total++;

				if ((Boolean) entity.getProperty(BasketMessage.PRESENCE)) {
					day.presence++;
				}
				if (!day.ball && (Boolean) entity.getProperty(BasketMessage.BALL)) {
					day.ball = true;
				}
			}
			dayRecordHashStore = messages;
		}
		return dayRecordHashStore;
	}

	protected static void clearOldMeassages() {
		Calendar calendar = Calendar.getInstance();
		calendar.add(Calendar.MONTH, -2);
		String dayTreshold = formatDate(calendar);

		Query query = new Query(BasketMessage.KIND);
		PreparedQuery pq = ds.prepare(query);
		QueryResultIterator<Entity> result = pq.asQueryResultIterator();

		Vector<Key> keysForDeletion = new Vector<Key>();
		while (result.hasNext()) {
			final Entity entity = result.next();
			final Key key = entity.getKey();
			String keyName = key.getName();

			if (dayTreshold.compareTo(keyName) > 0) {
				keysForDeletion.add(key);
			}
		}
		if (keysForDeletion.size() > 0) {
			ds.delete(keysForDeletion);
		}
	}

	/**
	 * Return list of all BasketMessage for given date
	 * 
	 * @param date
	 *            date to query for messages
	 * @return list of all BasketMessage for given date
	 */
	public static List<BasketMessage> getDayMessages(String date) {
		final List<BasketMessage> result = new ArrayList<BasketMessage>(16);
		final PreparedQuery pq = ds.prepare(new Query(BasketMessage.KIND, KeyFactory.createKey(date, date)));
		final QueryResultIterator<Entity> iterator = pq.asQueryResultIterator();

		while (iterator.hasNext()) {
			final Entity entity = iterator.next();
			final Key key = entity.getKey();
			final String kind = key.getKind();
			final Boolean presence = (Boolean) entity.getProperty(BasketMessage.PRESENCE);
			final Boolean ball = (Boolean) entity.getProperty(BasketMessage.BALL);
			final String message = entity.getProperty(BasketMessage.MESSAGE).toString();
			final String mail = entity.getProperty(BasketMessage.EMAIL).toString();

			BasketMessage bMessage = new BasketMessage(kind, mail, presence, ball, message, getUserName(mail));
			result.add(bMessage);
		}
		return result;
	}

	/**
	 * return user name for a given e-mail
	 * 
	 * @param email
	 * @return null if user email not registered
	 */
	public static String getUserName(String email) {
		if (email == null || email.length() <= 0) {
			return null;
		}
		Key key = KeyFactory.createKey(BasketUser.KIND, email.toLowerCase());
		try {
			Entity entity = ds.get(key);
			return entity.getProperty(BasketUser.NAME).toString();
		} catch (Exception e) {
		}
		return null;
	}

	/**
	 * check is a user e-mail is in the system
	 * 
	 * @param email
	 *            the user e-mail to be checked
	 * @return true - when valid
	 */
	public static boolean isValidUser(String email) {
		if (email == null || email.length() <= 0) {
			return false;
		}
		Key key = KeyFactory.createKey(BasketUser.KIND, email.toLowerCase());
		try {
			Entity entity = ds.get(key);
			return entity != null;
		} catch (EntityNotFoundException e) {
		}
		return false;
	}

	/**
	 * Return list of all BasketMessage for given date
	 * 
	 * @param date
	 *            date to query for messages
	 * @return list of all BasketMessage for given date
	 */
	public static synchronized List<String> getAllEMailUsers() {
		if (allUserMails == null) {
			final List<String> userMails = new ArrayList<String>(64);

			final PreparedQuery pq = ds.prepare(new Query(BasketUser.KIND));
			final QueryResultIterator<Entity> iterator = pq.asQueryResultIterator();

			while (iterator.hasNext()) {
				final Entity entity = iterator.next();
				final Key key = entity.getKey();
				final String email = key.getName();
				if (email != null && email.length() > 0 && email.contains("@")) {
					userMails.add(email);
				}
			}

			userMails.add("GeorgeRadev@gmail.com");
			allUserMails = userMails;
		}
		return allUserMails;
	}

	/**
	 * converts a date to String "YYY-MM-DD"
	 * 
	 * @param date
	 *            to convert
	 * @return String presentation if the date
	 */
	public static String formatDate(Calendar date) {
		if (date == null) {
			return "";
		}
		return formatDate(date.get(Calendar.YEAR), 1 + date.get(Calendar.MONTH), date.get(Calendar.DAY_OF_MONTH));
	}

	/**
	 * converts a date parameters to String "YYY-MM-DD"
	 * 
	 * @return String presentation if the date parameters
	 */
	public static String formatDate(final int year, final int month, final int day) {
		StringBuilder result = new StringBuilder(16);
		result.append(year);
		result.append('-');
		{
			if (month < 10)
				result.append('0');
			result.append(month);
		}
		result.append('-');
		{
			if (day < 10)
				result.append('0');
			result.append(day);
		}
		return result.toString();
	}

	/**
	 * Sends mail to all seems-to-be-valid email of the users
	 * 
	 * @param currentDate
	 *            date of the event
	 */
	public static void sendMailToAll(String currentDate) {
		List<String> mails = DBManager.getAllEMailUsers();

		Properties props = new Properties();
		Session session = Session.getDefaultInstance(props, null);

		final String messageSubject = "new BASKETBALL event [" + currentDate + "]";
		final String messageBody;
		{
			StringBuilder message = new StringBuilder();
			message.append("New basketball event at ").append(currentDate).append(".");
			message.append("\nsee : https://basket-scheduler.appspot.com/ ");
			message.append("\n\nPlease, do not reply to this letter!");
			messageBody = message.toString();
		}

		for (String mail : mails) {
			try {
				Message msg = new MimeMessage(session);
				msg.setFrom(new InternetAddress("GeorgeRadev@gmail.com", "George Radev"));
				msg.addRecipient(Message.RecipientType.TO, new InternetAddress(mail, mail));
				msg.setSubject(messageSubject);
				msg.setText(messageBody);
				Transport.send(msg);
			} catch (Exception e) {
				e.printStackTrace();
				log.log(Level.SEVERE, "problem while sending mail to " + mail, e);
			}
		}
	}
}
